/*
 *  Simbad - Robot Simulator
 *  Copyright (C) 2004 Louis Hugues
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful, 
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 -----------------------------------------------------------------------------
 * $Author: sioulseuguh $ 
 * $Date: 2005/08/07 12:25:03 $
 * $Revision: 1.16 $
 * $Source: /cvsroot/simbad/src/simbad/sim/World.java,v $
 */
package simbad.sim;

import java.awt.Dimension;
import java.awt.GraphicsConfigTemplate;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.image.BufferedImage;
import java.util.Map;
import javax.media.j3d.*;

import javax.vecmath.*;

import com.sun.j3d.utils.geometry.Primitive;
import com.sun.j3d.utils.geometry.Sphere;

/**
 * Represents a 3d world - this class use intensively JAVA3D. It creates the
 * scenegraph and the view platform. The building process uses a
 * EnvironmentDescription object containing colors , objects and attributes
 * given by the user. This class is thighly coupled with Simulator Class.
 * 
 * Remember to call System.setProperty("j3d.implicitAntialiasing", "true") at
 * the very beginning of your program in order to enable antialiasing.
 * 
 */
public class World {

	/** J3D universe */
	VirtualUniverse universe;
	/** The content branch. */
	private BranchGroup sceneBranch;
	/** The base translation of the sceneBranch. */
	private TransformGroup sceneTrans;
	/** The base rotation of the sceneBranch. */
	private TransformGroup sceneRot;

	/**
	 * All the pickable/collidable objects should be attached under this
	 * sub-branch
	 */
	private BranchGroup pickableSceneBranch;
	/** The view branch */
	private BranchGroup viewBranch;
	private TransformGroup viewTransformGroup;
	private ViewPlatform viewPlatform;
	private View view;

	/** The main canvas for viewing the world. */
	private Canvas3D canvas3d;
	Dimension offScreenViewDimensions;
	/** For managing the mouse mouvement in the Canvas3D */
	MouseOrbiter mouseOrbiter;

	// lights
	private Light light1;
	private Light light2;

	// constants used to specify the viewpoint
	public static final int VIEW_FROM_TOP = 1;
	public static final int VIEW_FROM_EAST = 2;
	public static final int VIEW_BEHIND_AGENT = 3;
	public static final int VIEW_ABOVE_AGENT = 4;
	public static final int VIEW_ABOVE_AGENT_NEAR = 5;
	public static final int VIEW_AGENT_SIDE = 6;

	/** The size of the square containing the world */
	protected float worldWidth, worldHeight;

	/** Prepared colors. */
	Color3f white = new Color3f(1, 1, 1);
	Color3f black = new Color3f(0, 0, 0);

	/** Construct a World from a given EnvironmentDescription. */
	public World(EnvironmentDescription ed) {
		create(ed);
	}

	/** Construct a World from a given EnvironmentDescription. */
	public World(EnvironmentDescription ed, Dimension offScreenViewDimensions) {
		this.offScreenViewDimensions = offScreenViewDimensions;
		create(ed);
	}

	/**
	 * Creates the world from the given environement Description. Used only in
	 * the creation phase.
	 * 
	 * @param ed
	 *            the environment description.
	 */
	private void create(EnvironmentDescription ed) {
		worldWidth = ed.worldWidth;
		worldHeight = ed.worldHeight;
		createUniverse(ed);

	}

	/**
	 * Creates the universe to attach the scenegraph. Used only in the creation
	 * phase.
	 * 
	 * @param ed
	 *            the environment description.
	 */
	private void createUniverse(EnvironmentDescription ed) {
		/*
		System.out.println("create Universe");
		// show infos
		Map map = VirtualUniverse.getProperties();
		System.out.println("----------------------------------------");
		System.out.println("j3d.version = " + map.get("j3d.version"));
		System.out.println("j3d.vendor = " + map.get("j3d.vendor"));
		System.out.println("j3d.specification.version = " + map.get("j3d.specification.version"));
		System.out.println("j3d.specification.vendor = " + map.get("j3d.specification.vendor"));
		System.out.println("j3d.renderer = " + map.get("j3d.renderer"));
		System.out.println("J3DThreadPriority = " + VirtualUniverse.getJ3DThreadPriority());
		System.out.println("----------------------------------------");
		 */
		createCanvas3D();
		createSceneBranch(ed);

		universe = new VirtualUniverse();

		Locale locale = new Locale(universe);

		// Create and add VIEW branch
		// locale->viewBranch->viewTransformGroup->viewPlatform
		viewBranch = new BranchGroup();
		viewTransformGroup = new TransformGroup();
		viewTransformGroup.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
		viewTransformGroup.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
		Transform3D t3d = new Transform3D();
		t3d.setIdentity();
		viewTransformGroup.setTransform(t3d);
		viewBranch.addChild(viewTransformGroup);

		// Creates View and viewplatform
		viewPlatform = new ViewPlatform();
		viewPlatform.setViewAttachPolicy(View.NOMINAL_HEAD);
		viewPlatform.setActivationRadius(100);
		view = new View();
		view.setProjectionPolicy(View.PERSPECTIVE_PROJECTION);
		//view.setProjectionPolicy(View.PARALLEL_PROJECTION);

		view.setViewPolicy(View.SCREEN_VIEW);
		view.setVisibilityPolicy(View.VISIBILITY_DRAW_ALL);

		view.setFrontClipDistance(0.02);

		GraphicsConfigTemplate3D template = new GraphicsConfigTemplate3D();
		template.setSceneAntialiasing(GraphicsConfigTemplate.REQUIRED);
		template.setDoubleBuffer(GraphicsConfigTemplate.PREFERRED);
		/*
		 * GraphicsConfiguration config = GraphicsEnvironment
		 * .getLocalGraphicsEnvironment().getDefaultScreenDevice()
		 * .getBestConfiguration(template);
		 */
		// request antialiasing
		view.setSceneAntialiasingEnable(true);

		
		view.addCanvas3D(canvas3d);
		PhysicalBody phyBody = new PhysicalBody();
		PhysicalEnvironment phyEnv = new PhysicalEnvironment();
		view.setPhysicalBody(phyBody);
		view.setPhysicalEnvironment(phyEnv);
		view.attachViewPlatform(viewPlatform);
		viewTransformGroup.addChild(viewPlatform);

		// Add both branch to the unique locale
		locale.addBranchGraph(viewBranch);
		locale.addBranchGraph(sceneBranch);
		
		// Add mouse control in the canvas3d
		if (ed.enableMouseControl)
			mouseOrbiter = new MouseOrbiter(canvas3d, viewTransformGroup);
		
		//System.out.println(universe.getProperties());
		
		// sets initial viewpoint
		changeViewPoint(ed.worldViewPoint, null);
	}

	/**
	 * Creates the Canvas3D to visualize the 3D World. Used only in the creation
	 * phase.
	 */
	private void createCanvas3D() {
		GraphicsConfigTemplate3D template = new GraphicsConfigTemplate3D();
		template.setSceneAntialiasing(GraphicsConfigTemplate.PREFERRED);
		GraphicsConfiguration config = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getBestConfiguration(template);
		// create canvas
		canvas3d = new Canvas3D(config, offScreenViewDimensions != null);
		//canvas3d.setDoubleBufferEnable(true);
		if (offScreenViewDimensions != null) {
			BufferedImage image = new BufferedImage(offScreenViewDimensions.width, offScreenViewDimensions.height, BufferedImage.TYPE_INT_RGB);
			ImageComponent2D buffer = new ImageComponent2D(ImageComponent.FORMAT_RGB, image);
			buffer.setCapability(ImageComponent2D.ALLOW_IMAGE_READ);
			canvas3d.setOffScreenBuffer(buffer);
			canvas3d.getScreen3D().setSize(offScreenViewDimensions.width, offScreenViewDimensions.height);
			if (offScreenViewDimensions.width > offScreenViewDimensions.height) {
				canvas3d.getScreen3D().setPhysicalScreenWidth(0.5);
				canvas3d.getScreen3D().setPhysicalScreenHeight(((double)offScreenViewDimensions.height/offScreenViewDimensions.width)/2);
			}
			else {
				canvas3d.getScreen3D().setPhysicalScreenWidth(((double)offScreenViewDimensions.width/offScreenViewDimensions.height)/2);
				canvas3d.getScreen3D().setPhysicalScreenHeight(0.5);
			}
		}
			
/*
		// display j3d info
		Map map = canvas3d.queryProperties();
		System.out.println("doubleBufferAvailable = " + map.get("doubleBufferAvailable"));
		System.out.println("sceneAntialiasingNumPasses = " + map.get("sceneAntialiasingNumPasses"));
		System.out.println("sceneAntialiasingAvailable = " + map.get("sceneAntialiasingAvailable"));
		System.out.println("texture3DAvailable = " + map.get("texture3DAvailable"));
*/
	}

	/**
	 * Adds a 3D object to the world. Used only in the creation phase.
	 */
	public void addObjectToPickableSceneBranch(BaseObject obj3d) {
		obj3d.compile();
		pickableSceneBranch.addChild(obj3d.getNode());
	}

	/** Detach a previously attached object from the scenegraph. */
	public void detach(BaseObject obj3d) {
		pickableSceneBranch.removeChild(obj3d.getNode());
	}

	/** attach a 3d object to the scenegraph. */
	public void attach(BaseObject obj3d) {
		pickableSceneBranch.addChild(obj3d.getNode());
	}

	/**
	 * Add a light to the 3d world. Used only in the creation phase.
	 */
	Light addLight(Vector3d pos, Color3f color) {
		BoundingSphere bounds = new BoundingSphere(new Point3d(0.0, 0.0, 0.0), Math.max(worldWidth, worldHeight) * 2);

		TransformGroup tg = new TransformGroup();
		Transform3D t3d = new Transform3D();
		t3d.set(pos);
		tg.setTransform(t3d);
		PointLight light = new PointLight();
		light.setAttenuation(0.5f, 0, 0);
		// light.setAttenuation(0f,.08f,0);
		// light.setAttenuation(1.2f,0,0);
		// note : light pos not affected by transform (but bound is).
		light.setPosition((float) pos.x, (float) pos.y, (float) pos.z);
		light.setInfluencingBounds(bounds);
		sceneTrans.addChild(light);
		// light geometry
		ColoringAttributes ca = new ColoringAttributes();
		ca.setColor(color);
		Appearance appL1 = new Appearance();
		appL1.setColoringAttributes(ca);
		Primitive s = new Sphere(0.4f, appL1);
		s.setCollidable(true);
		tg.addChild(s);
		sceneTrans.addChild(tg);
		return light;
	}

	/**
	 * Creates the branch for the visible content of the scenegraph. Used only
	 * in the creation phase.
	 */
	private void createSceneBranch(EnvironmentDescription wd) {

		sceneBranch = new BranchGroup();
		sceneRot = new TransformGroup();
		sceneRot.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
		sceneRot.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
		sceneRot.setCapability(TransformGroup.ALLOW_CHILDREN_EXTEND);
		// add transform
		sceneTrans = new TransformGroup();
		// allow transform access
		sceneTrans.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
		sceneTrans.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
		sceneTrans.setCapability(TransformGroup.ALLOW_CHILDREN_EXTEND);
		sceneBranch.addChild(sceneRot);
		sceneRot.addChild(sceneTrans);

		// bounds (lights,background, behaviors)
		BoundingSphere bounds = new BoundingSphere(new Point3d(0.0, 0.0, 0.0), Math.max(worldWidth, worldHeight) * 3);

		// background
		Color3f bgColor = wd.backgroundColor;
		Background bgNode = new Background(bgColor);
		bgNode.setApplicationBounds(bounds);
		sceneTrans.addChild(bgNode);

		// ambient light
		TransformGroup tga = new TransformGroup();
		AmbientLight ambientLight = new AmbientLight(wd.ambientLightColor);
		ambientLight.setInfluencingBounds(bounds);
		tga.addChild(ambientLight);
		sceneBranch.addChild(tga);

		// directional lights
		if (wd.light1Position != null) {
			light1 = addLight(wd.light1Position, wd.light1Color);
			light1.setEnable(wd.light1IsOn);
		}
		if (wd.light2Position != null) {
			light2 = addLight(wd.light2Position, wd.light2Color);
			light2.setEnable(wd.light2IsOn);
		}

		createFloor(wd);
		if (wd.hasAxis)
			createAxis();

		pickableSceneBranch = new BranchGroup();
		sceneTrans.addChild(pickableSceneBranch);
		pickableSceneBranch.setCapability(BranchGroup.ALLOW_CHILDREN_EXTEND);
		pickableSceneBranch.setCapability(BranchGroup.ALLOW_DETACH);
		pickableSceneBranch.setCapability(BranchGroup.ALLOW_CHILDREN_WRITE);

	}

	/**
	 * Creates the floor of the 3d world. Used only in the creation phase.
	 * 
	 * @param ed
	 *            the environment description.
	 */
	private void createFloor(EnvironmentDescription wd) {

		float minx = -worldWidth / 2, maxx = worldWidth / 2;
		float minz = -worldHeight / 2, maxz = worldHeight / 2;

		Point3f[] floorCoords = { new Point3f(minx, 0.0f, minz), new Point3f(minx, 0.0f, maxz), new Point3f(maxx, 0.0f, maxz), new Point3f(maxx, 0.0f, minz) };
		Vector3f[] floorNormals = { new Vector3f(-0.6f, 0.6f, -0.6f), new Vector3f(-0.6f, 0.6f, 0.6f), new Vector3f(0.6f, 0.6f, 0.6f),
		        new Vector3f(0.6f, 0.6f, -0.6f) };
		Vector3f[] floorNormalsSimple = { new Vector3f(0, 1, 0), new Vector3f(0, 1, 0), new Vector3f(0, 1, 0), new Vector3f(0, 1, 0) };

		QuadArray floorQuads = null;
		switch (wd.normalsStyle) {
		case EnvironmentDescription.NORMALS_REALISTIC:
			floorQuads = new QuadArray(floorCoords.length, GeometryArray.COORDINATES | GeometryArray.NORMALS);
			floorQuads.setNormals(0, floorNormals);
			break;
		case EnvironmentDescription.NORMALS_SIMPLE:
			floorQuads = new QuadArray(floorCoords.length, GeometryArray.COORDINATES | GeometryArray.NORMALS);
			floorQuads.setNormals(0, floorNormalsSimple);
			break;

		}
		floorQuads.setCoordinates(0, floorCoords);
		Appearance floorAppear = new Appearance();

		Material mat = new Material();
		//Color3f specular = new Color3f(wd.floorColor);
		//mat.setDiffuseColor(wd.floorColor);
		//specular.scale(1.1f);
		//specular.clampMax(0.8f);
		//mat.setSpecularColor(specular);
		mat.setDiffuseColor(wd.floorColor);
		mat.setAmbientColor(wd.floorColor);
		mat.setSpecularColor(new Color3f(0,0,0));
		floorAppear.setMaterial(mat);
		Shape3D floor = new Shape3D(floorQuads, floorAppear);
		floor.setPickable(false);
		floor.setCollidable(false);

		sceneTrans.addChild(floor);
	}

	/**
	 * Creates a representation of the 3 axis of the 3d world. Used only in the
	 * creation phase.
	 */
	private void createAxis() {
		Point3f[] axisCoords = {
		        // X axis arrow
		        new Point3f(0.0f, 0.001f, 0.0f), new Point3f(1, 0.001f, 0.0f), new Point3f(1, 0.001f, 0.0f),
		        new Point3f(0.95f, 0.001f, 0.05f),
		        new Point3f(1, 0.001f, 0.0f),
		        new Point3f(0.95f, 0.001f, -0.05f),

		        // a small X
		        new Point3f(1.0f, 0.001f, 0.1f), new Point3f(0.9f, 0.001f, 0.2f),
		        new Point3f(1.0f, 0.001f, 0.2f),
		        new Point3f(0.9f, 0.001f, 0.1f),
		        // Z axis arrow
		        new Point3f(0.0f, 0.001f, 0.0f), new Point3f(0, 0.001f, 1.0f), new Point3f(0, 0.001f, 1.0f), new Point3f(0.05f, 0.001f, 0.95f),
		        new Point3f(0, 0.001f, 1.0f),
		        new Point3f(-0.05f, 0.001f, 0.95f),

		        // a small Z
		        new Point3f(0.1f, 0.001f, 1.0f), new Point3f(0.2f, 0.001f, 1.0f), new Point3f(0.1f, 0.001f, 0.9f), new Point3f(0.2f, 0.001f, 0.9f),
		        new Point3f(0.1f, 0.001f, 1.0f), new Point3f(0.2f, 0.001f, 0.9f),
		        // Y axis arrow
		        new Point3f(0.0f, 0.001f, 0.0f), new Point3f(0, 1.0f, 0.0f), new Point3f(0, 1.0f, 0.0f), new Point3f(0.05f, 0.95f, 0f), new Point3f(0, 1f, 0f),
		        new Point3f(0.00f, 0.95f, 0.05f),

		        // a small Y
		        new Point3f(0.2f, 1f, 0.0f), new Point3f(0.1f, 0.9f, 0f), new Point3f(0.1f, 1.0f, 0.0f), new Point3f(0.15f, 0.95f, 0.0f)

		};
		// scale axis drawing to 5% of word size
		for (int i = 0; i < axisCoords.length; i++) {
			axisCoords[i].scale(worldWidth * 0.05f);
		}

		LineArray axisLines = new LineArray(axisCoords.length, GeometryArray.COORDINATES);
		axisLines.setCoordinates(0, axisCoords);

		Appearance axisAppear = new Appearance();
		ColoringAttributes ca = new ColoringAttributes();
		ca.setColor(white);
		axisAppear.setColoringAttributes(ca);
		Material mat = new Material();
		mat.setDiffuseColor(white);
		axisAppear.setMaterial(mat);
		Shape3D axis = new Shape3D(axisLines, axisAppear);
		axis.setCollidable(false);
		axis.setPickable(false);
		sceneTrans.addChild(axis);

	}

	/**
	 * Change the user view Point . Note that we modify the ViewBranch transform
	 * not the scene transform.
	 * 
	 * @param type
	 *            can be VIEW_FROM_TOP,VIEW_FROM_EAST,VIEW_BEHIND_AGENT
	 * @param agent
	 *            : specify the agent if VIEW_BEHIND_AGENT
	 * 
	 *            The VIEW_BEHIND_AGENT case has to be called regularly because
	 *            of the agent displacement.
	 */

	public void changeViewPoint(int type, SimpleAgent agent) {
		Point3d p1 = new Point3d();
		Point3d p2 = new Point3d();

		Transform3D t1 = new Transform3D();
		Transform3D t2 = new Transform3D();
		t1.setIdentity();
		t2.setIdentity();
		if (mouseOrbiter != null)
			mouseOrbiter.resetView();
		switch (type) {
		case VIEW_FROM_TOP:
			t1.lookAt(new Point3d(0, Math.max(worldWidth, worldHeight) * 1.2, 0), new Point3d(0, 0, 0), new Vector3d(0, 0, -1));
			t1.invert();
			viewTransformGroup.setTransform(t1);
			break;
		case VIEW_FROM_EAST:
			t1.lookAt(new Point3d(worldWidth, worldHeight, 0), new Point3d(0, 0, 0), new Vector3d(-1, 0, 0));
			t1.invert();
			viewTransformGroup.setTransform(t1);
			break;
		case VIEW_BEHIND_AGENT:
			t1.setTranslation(new Vector3d(-agent.getRadius() * 2, 0, 0));
			agent.getGroup().getLocalToVworld(t2);
			t1.mul(t2);
			viewTransformGroup.setTransform(t1);
			break;
		case VIEW_ABOVE_AGENT:
			agent.getRotationTransformGroup().getLocalToVworld(t1);
			t1.transform(p1);
			t1.transform(p2);
			p2.y = Math.max(worldWidth, worldHeight) * .8;
			t1.lookAt(p2, p1, new Vector3d(0, 0, -1));
			t1.invert();
			viewTransformGroup.setTransform(t1);
			break;

		case VIEW_ABOVE_AGENT_NEAR:
			agent.getRotationTransformGroup().getLocalToVworld(t1);
			t1.transform(p1);
			t1.transform(p2);
			p2.y = agent.getHeight() * Math.max(worldWidth, worldHeight) * 0.5;
			// avoid front clipping
			if (p2.y < 0.2)
				p2.y = 0.2;
			t1.lookAt(p2, p1, new Vector3d(0, 0, -1));
			t1.invert();
			viewTransformGroup.setTransform(t1);

			break;
		case VIEW_AGENT_SIDE:

			agent.getRotationTransformGroup().getLocalToVworld(t1);
			t1.transform(p1);
			t1.transform(p2);
			agent.rotation.transform(p2);
			t2.setTranslation(new Vector3d(0, agent.getHeight() * 2, agent.getRadius() * 10));
			t2.transform(p2);
			t1.lookAt(p2, p1, new Vector3d(0, 1, 0));
			t1.invert();
			viewTransformGroup.setTransform(t1);

			break;
		}
	}

	boolean checkCollisionAgainstBlockWorldObjects(BoundingSphere bs) {
		return false;
	}

	/** Stop rendering on main canvas 3D . Used for background mode. */
	public void stopRendering() {
		canvas3d.stopRenderer();
	}

	/** Restart rendering on main canvas 3D . Used for background mode. */
	public void startRendering() {
		canvas3d.startRenderer();
	}

	/** Do one rendering on main canvas 3D . Used for background mode. */
	public void renderOnce() {
		canvas3d.startRenderer();

		try {
			Thread.sleep((int) (100));
		} catch (InterruptedException e) {

			e.printStackTrace();
		}
		canvas3d.stopRenderer();
	}
	
	/* Render using off-screen buffer. Returns when rendering is complete. */
	public BufferedImage renderToImage() {
		try {
			canvas3d.renderOffScreenBuffer();
		} catch (RestrictedAccessException e) {
			System.err.println("RestrictedAccessException");
		}
		canvas3d.waitForOffScreenRendering();
		
		// copy rendered image
		return canvas3d.getOffScreenBuffer().getImage();
	}
	

	/** Destroy the java3d graph */
	public void dispose() {
		universe.removeAllLocales();
		view.removeAllCanvas3Ds();
	}

	/**
	 * @return the canvas3D associated to the world view platform
	 */
	public Canvas3D getCanvas3D() {
		return canvas3d;
	}

	/**
	 * @return the scene branchgroup containing the world's object which can be
	 *         picked
	 */
	BranchGroup getPickableSceneBranch() {
		return pickableSceneBranch;
	}

}